Skip to content

MCP server integration + per-conversation vault binding#2

Merged
RaghavSood merged 7 commits intomainfrom
feature/mcp-vault-info
Feb 24, 2026
Merged

MCP server integration + per-conversation vault binding#2
RaghavSood merged 7 commits intomainfrom
feature/mcp-vault-info

Conversation

@RaghavSood
Copy link
Collaborator

Summary

  • Adds an MCP (Model Context Protocol) JSON-RPC 2.0 client that connects to the Vultisig MCP server, discovers tools (e.g. get_eth_balance, get_token_balance, set_vault_info), and exposes them to Claude alongside existing built-in tools
  • Introduces a set_vault built-in tool that stores vault keys (ecdsa_public_key, eddsa_public_key, chaincode_hex) per-conversation in Postgres, so the MCP server can derive addresses automatically
  • On every message request, automatically primes the MCP session with the active vault — no manual re-sending of keys by the user

Why this is needed

Users have multiple vaults and switch between them. When Claude calls MCP tools like get_eth_balance, the MCP server needs to know which vault's keys to use for address derivation. Without per-conversation tracking, the user would need to re-provide keys every time, or worse, the wrong vault's balance could be returned.

By binding vault keys to the conversation:

  1. The user (or app) calls set_vault once when selecting a vault
  2. Every subsequent message automatically primes the MCP session with those keys
  3. If the user switches vaults mid-conversation, calling set_vault again updates both the DB and the MCP session
  4. Claude sees the active vault in the system prompt, so it can reference the keys contextually

How it works

MCP client (internal/mcp/client.go)

  • Lightweight JSON-RPC 2.0 over Streamable HTTP, no external SDK
  • Initialize()ListTools() at startup; tools cached in-memory with configurable TTL
  • Stale cache fallback: if a refresh fails, the last-known tools are still served
  • Session ID tracked via Mcp-Session-Id header
  • Structured logging at every interaction point for debuggability

Vault lifecycle

App sends vault keys → Claude calls set_vault → stored in agent_conversations
                                               → forwarded to MCP set_vault_info

Next message arrives → ProcessMessage loads conversation
                     → vault info found → auto-calls MCP set_vault_info
                     → Claude calls get_eth_balance → MCP derives address from vault keys

Configuration

Env var Default Description
MCP_SERVER_URL (empty = disabled) MCP server endpoint (e.g. http://localhost:8888)
MCP_TOOL_CACHE_TTL_SECONDS 300 How long to cache discovered MCP tools

Database

New migration adds three nullable columns to agent_conversations:

  • ecdsa_public_key TEXT
  • eddsa_public_key TEXT
  • chaincode_hex TEXT

Test plan

  • Set MCP_SERVER_URL=http://localhost:8888, start server — logs should show mcp tools loaded with tool count
  • Unset MCP_SERVER_URL, start server — no MCP logs, server works normally
  • Send a message with vault keys via set_vault tool — verify DB columns populated, MCP set_vault_info called
  • Send follow-up message — verify MCP session auto-primed (debug log: mcp session primed with vault info)
  • Call set_vault again with different keys — verify DB updated, new keys forwarded to MCP
  • Ask Claude "what tools do you have?" — should list both built-in and MCP tools
  • Trigger an MCP tool (e.g. ask for ETH balance) — verify MCP tool called and result returned

🤖 Generated with Claude Code

@RaghavSood RaghavSood mentioned this pull request Feb 22, 2026
5 tasks
Copy link
Contributor

@neavra neavra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments! Please take a look

})

// Agent routes (authenticated)
agent := e.Group("/agent", server.AuthMiddleware)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent routes should be authenticated so that only vault owners can access conversations

}

// SetVaultTool sets the active vault for this conversation.
var SetVaultTool = anthropic.Tool{
Copy link
Contributor

@neavra neavra Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to do on creation of a new conversation? Since the conversation is tied to a vault we should be able to get the needed keys on conversation creation? Let me know if im misinterpreting something

RaghavSood and others added 6 commits February 24, 2026 13:25
Connects the agent backend to a Vultisig MCP server (optional, via
MCP_SERVER_URL) so Claude can discover and call external tools like
get_eth_balance and get_token_balance.

Adds a set_vault built-in tool that binds vault keys (ECDSA, EdDSA,
chaincode) to a conversation. On each request the MCP session is
automatically primed with the active vault so address derivation
happens server-side without the user repeating keys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add nix flake dev environment with Go, PostgreSQL, Redis, sqlc
- Add Transaction type and extract transactions from MCP tool results
- Remove redundant public key auth checks (already handled by middleware)
- Rename chaincode_hex to chain_code for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract token search results from the find_token MCP tool and pass them
as structured data in the API response so frontend apps can prompt users
to add tokens/chains to their vault.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues prevented the tokens field from appearing in API responses:

1. CallTool discarded text on IsError — when the MCP tool set IsError: true,
   CallTool returned a Go error and the text content was lost. Now returns
   a ToolError that carries the text, so executeTool can still pass it to
   trackToolResult for structured extraction.

2. trackToolResult assumed pure JSON — MCP tools may return multiple text
   content blocks (joined with \n) or mix descriptive text with JSON. The
   direct json.Unmarshal failed silently. Now uses extractTokens() which
   tries direct unmarshal first, then scans for JSON objects in the text
   using json.Decoder (which handles trailing content).

Also adds diagnostic logging when parsing fails so we can see the actual
MCP result text in logs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Integrate MCP resources protocol (resources/list, resources/read) to
discover and load skill guides from the MCP server. Skills are markdown
documents at skills/{slug}.md that provide detailed workflow instructions.

The skill list is injected into the system prompt so the LLM knows what's
available, but skill content is only loaded on-demand via the new get_skill
tool when relevant to the user's request. This keeps the context window
lean as the skill library grows.

- MCP client: add ListSkills, ReadSkill, SkillSummary with TTL caching
- Agent: extend MCPToolProvider interface with skill methods
- Agent: inject skill summary into system prompt after tool descriptions
- Tools: add get_skill native tool (only registered when skills exist)
- Executor: add get_skill handler delegating to MCP ReadSkill
- Main: pre-warm skill cache at startup alongside tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The MCP server returns skill resources with URIs like
"skill://vultisig/evm-contract-call.md" but extractSkillSlug was
filtering for the "skills/" prefix, discarding all entries.

- extractSkillSlug now extracts the last path segment before .md,
  handling any URI scheme (skill://, skills/, etc.)
- ReadSkill now looks up the full URI from the skill cache instead
  of constructing it, so it works with any URI format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@RaghavSood RaghavSood force-pushed the feature/mcp-vault-info branch from 0ee21c3 to ece7411 Compare February 24, 2026 05:26
…rmat

OpenRouter's universal format is OpenAI Chat Completions — it translates
that to whatever the underlying model needs. Non-Anthropic models were
rejecting our Anthropic Messages API format with "Invalid Anthropic
Messages API request".

- Rewrite client.go types and wire format (ToolCall, AssistantMessage,
  ToolMessage, ToolChoice with custom MarshalJSON)
- URL: /messages → /chat/completions
- System prompt: moved from request field to system message
- Tool definitions: InputSchema wrapped as function parameters on wire
- Response: parsed from choices[0].message with convenience fields
- Streaming: OpenAI SSE format (data: chunks, data: [DONE])
- Update agent.go ProcessMessage/ProcessMessageStream for new types
- Rename GetAnthropicTools → GetTools in MCP client and interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@RaghavSood RaghavSood merged commit ed20297 into main Feb 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants